Face SDF
全库目录221
本文目录8
鼻翼和眼窝的真实阴影很细碎,原画却常用一块连续暗面。Face SDF 把设计好的明暗边界存成可比较的灰度场,头部局部光方向决定阈值。灰度值的含义、左右通道与头部坐标对齐,明暗才会随转头连续。
灰度场与角度契约
这里的 SDF 是风格化材质中的习惯叫法。严格的有符号距离场保存到边界的距离,单位可以是纹素或长度;本页模型保存每个纹素的熄灭角度,数值在 0 到 1 之间,不宣称是空间距离。它适合用单个阈值扫出一族连续边界。
设头部局部 X 轴向右,Y 轴向上,Z 轴向前。光方向从表面指向光源,头部变换仅保留旋转。将光投影到局部 XZ 平面,以正面为 0、背面为 1:
贴图 R 通道表示从左侧转到背面的一族边界,G 通道表示从右侧转到背面的一族边界。通道值 表示该纹素保持亮面的最大归一化角度: 为亮, 为暗。例如 的面颊在侧光 时仍亮,到偏背光 时变暗。这给美术绘制和运行时比较提供了同一判据。
这种编码要求某一侧旋转过程中每个纹素只跨越边界一次。若鼻侧需要先亮、再暗、又变亮,单通道单阈值不能表达,需要分角度贴图或额外遮罩。编码能力由数据模型决定,增加纹理分辨率不能解决这个限制。
角度场制作
运行时只有一个角度阈值,贴图必须把一组美术明暗图压缩成同一种顺序。先固定头部姿态和UV,在正面、四分之一、侧面、四分之三和背面来光时,各画一张二值亮区图。左右两侧分别制作;所有图中同一纹素始终对应同一个脸部位置。
取面颊上的一个纹素,五个角度依次为0、0.25、0.5、0.75、1,画出的亮暗为1、1、1、0、0。它最后一次亮在0.5,第一次暗在0.75,因此熄灭角只能确定在这个区间。取中点就得到d=0.625:阈值0.6时它亮,0.7时它暗。这里0.25是角度区间宽度,增加纹理分辨率不会缩小这份角度不确定性;增加这个区间内的设计姿态才会。
对每个纹素执行相同扫描,就得到一张角度场。下面是本页参考工具的核心,输入角度严格递增、覆盖0到1,lit只能取0或1:
if any(next_value > value for value, next_value in zip(lit, lit[1:])):
raise ValueError("该纹素存在暗后再亮,单阈值无法表达")
last_lit = lit.index(0) - 1
threshold = (angles[last_lit] + angles[last_lit + 1]) * 0.5完整工具还处理全亮与全暗,分别编码成1与0。运行时采用软比较,恰好等于端点的像素会落在过渡中心;需要端点严格全亮或全暗时,可在合成中明确固定正面/背面端点,或另外记录始终亮暗标志,不能依靠加大软边宽度修正。
如果另一个鼻侧纹素画成1、0、1、0、0,扫描会直接报错。把这些输入硬压成一个灰度值,总有一个角度的设计会丢失。此时可以调整原画使边界单调,或者保存分段角度图:运行时找到包围当前角度的两张图,再插值它们的明暗。后者增加纹理与采样成本,却能保留多次跨越。单角度场适合哪张脸,由设计的边界族决定。
制作后先对每侧重建全部输入角度,并显示与原始二值图的差异。上述纹素在0.5时满足0.625>0.5,在0.75时满足0.625<0.75,两个输入都能重建;中间的0.625是模型选择的边界位置。对二值图做普通空间模糊会改变相邻纹素的熄灭角,因此应在重建预览中检查鼻翼边界是否偏移。采样角之间的边界移动是这个简化模型的插值选择,没有额外原画就无法证明它符合设计。压缩、Mip和UV过滤再分别加入,才能把编码损失与采样损失分开。
方向求值与边界过滤
下面是像素阶段 HLSL 机制片段。worldToHeadRotation 为无缩放的逆旋转矩阵;纹理按线性数据采样,关闭 sRGB。R/G 两个通道遵守上述左右契约,verticalThreshold 是光线接近头顶时使用的艺术参数。
float3 lightHead = mul(worldToHeadRotation, lightDirWS);
float2 horizontal = lightHead.xz;
float angleThreshold = verticalThreshold;
if (dot(horizontal, horizontal) > 1e-8)
angleThreshold = abs(atan2(horizontal.x, horizontal.y)) / 3.14159265;
float sdf = lightHead.x < 0.0 ? faceSdf.r : faceSdf.g;
float width = max(max(fwidth(sdf - angleThreshold), userSoftness), 1e-5);
float faceLit = smoothstep(angleThreshold - width,
angleThreshold + width, sdf);水平分量为零时不存在左右角度,因此不能靠归一化得到答案。固定阈值可让静态模型有确定输出。动画需要连同左右通道一起处理,否则光线从头顶的一侧越过另一侧,角度与通道仍会突然切换。直接保存上一帧方向还会引入历史状态,多个相机需要各自管理,不能把偶然残留的数据当作方向。
一种无历史状态的艺术处理是先把单位局部光的水平长度记为h=length(lightHead.xz),选水平过渡范围e=0.1。以w=smoothstep(0,e,h)混合“左右场平均值与固定竖直阈值比较”的亮度和正常角度亮度:
float h = length(lightHead.xz); // lightDirWS为单位方向,旋转矩阵保持长度
float verticalField = (faceSdf.r + faceSdf.g) * 0.5;
float verticalWidth = max(max(fwidth(verticalField - verticalThreshold),
userSoftness), 1e-5);
float verticalLit = smoothstep(verticalThreshold - verticalWidth,
verticalThreshold + verticalWidth, verticalField);
faceLit = lerp(verticalLit, faceLit, smoothstep(0.0, 0.1, h));竖直分支独立求verticalWidth,因为普通分支的导数还包含退化方位与通道选择;沿用它会把方位跳变带进竖直亮度。这里的导数在分支外计算。
当h=0,两侧任意方位都得到同一个verticalLit;当h>=0.1,结果完全由正常角度模型决定。这里混合的是最终亮度,因此同时压低左右通道跳变和角度退化的影响。范围0.1是单位方向下的示例艺术参数,平均场也只是确定的竖直设计选择;需要专门的顶光轮廓时,应给它单独的贴图或遮罩。
头部局部方向还应独立于对象缩放。例如世界光为(1,0,1)、头部没有旋转时,归一化角是0.25。如果把X轴缩放2倍的对象逆矩阵直接当作逆旋转使用,方向变成(0.5,0,1),阈值约为0.148;只是把模型拉宽,明暗边界就被错误地推向正面。先得到无缩放的头部朝向,再变换光方向,才能保持本页角度契约。
两通道在正前与正后方向交接。如果端点的明暗不同,通道选择一变,鼻梁就会跳色;制作时应约束两端结果一致。镜像角色也必须同步处理局部轴与左右贴图。只有纹理明确编码了对称关系时,才可用 1-u 代替双通道。
fwidth 把阈值差在像素内的变化转成软边宽度,减少细边闪烁。贴图压缩会改变阈值场,mip平均也会移动边界。假设相邻两个纹素的熄灭角是0.2和0.8,阈值0.4时二者的二值亮度为0和1,平均覆盖率为0.5;先平均角度场得到0.5,再与0.4比较却会整片判亮。阈值比较是非线性操作,平均输入与平均输出不等价。fwidth只能缓和屏幕采样边缘,不能恢复被Mip平均抹掉的角度分布。因此近景、远景和动态分辨率都需检查。增加 UV 岛的边缘扩展宽度可以防止 UV 岛外的无关值渗入,无法修复错误的颜色空间或阈值方向。
艺术分区与实际遮挡
Face SDF 决定脸部设计明暗,法线仍可参与面颊的柔和层次,AO 保存眼窝等局部遮蔽。头发和外部物体的投影由真实可见性提供。将这些量都命名为“阴影”再反复相乘,会把暗面压黑,失去原画的色彩。
可以先由 faceLit 在设计的亮色和暗色间插值,再由角色阴影可见性调整直接光层。例如设设计暗色为(0.2,0.1,0.1)、亮色为(1,0.7,0.6),faceLit=0.5时设计颜色是(0.6,0.4,0.35)。本页示例把它作为直接光层,以真实可见性V控制该层,再加环境层A:C=A+V*lerp(darkColor,lightColor,faceLit)。若V=0,仍保留环境色;把AO再乘到整个结果则连环境层也会压暗。固定AO参与哪一层属于材质规则,需用各层单独显示来确认。编辑面部法线时保留鼻翼和眼窝的必要结构,并检查它与法线贴图组合后的方向。
验证与证据
固定头部,使主光绕局部 Y 轴旋转,依次显示局部光方向、阈值、通道和亮区。正面、侧面、背面的阈值应为 0、0.5、1;头部与光一起旋转时结果应保持一致,因为局部方向没变。再测试镜像、非均匀对象缩放和竖直来光,确认它们各自遵守输入契约。
2026-10-22 执行 CPU参考测试通过,两项检查覆盖角度阈值和竖直回退。该实验验证本页定义的艺术模型,不代表某个商业游戏的内部实现。编译检查已用 DXC 将方向求值片段编译为 ps_6_0。贴图资产效果、引擎运行和 GPU 性能尚未执行。
角度场参考实验
从仓库根目录执行python -B 知识库/examples/mechanism_depth/face_sdf/test_reference.py。2026年10月23日本机执行5项检查通过,覆盖0.5到0.75的编码区间、暗后再亮拒绝、全亮全暗端点、近竖直左右方向收敛和正常侧光。CPU模型使用固定0.01软边,检查方向退化的标量关系,未覆盖GPU像素组的导数与动态控制流。工具保留区间宽度供制作诊断,未生成商业角色资产,也未执行引擎画面或GPU测试。读者可以把输入改成非单调序列,观察拒绝位置,再解释为什么单阈值无法重建原图。
相关主题
参考资料
- 本仓库 reference.py 的
face_threshold与test_reference.py:角度场模型、端点与退化输入的可复现实验。贴图契约是本文明确设计的示例。 - Microsoft,fwidth:屏幕导数定义;2026年10月23日核读定义
abs(ddx(x))+abs(ddy(x))及像素阶段约束;API说明支持过滤操作,特定角色的美术设计由资产验证。